백그라운드 실행 = 쓰레드?
"백그라운드로 실행한다"는 말이 곧 쓰레드를 쓴다는 뜻일까? 아닙니다. 백그라운드 실행에는 여러 방식이 있고, 쓰레드는 그중 하나일 뿐입니다.
이 포스트에서는 백그라운드 실행의 3가지 방식을 정리하고, 실제 ROS2 환경에서 어떻게 적용되는지 살펴봅니다.
백그라운드 실행의 3가지 방식
백그라운드에서 작업이 돌아가는 메커니즘은 크게 쓰레드, 프로세스, OS 스케줄링 세 가지로 나눌 수 있습니다.
flowchart LR
BG["🔄 백그라운드 실행"]
T["쓰레드\n(Thread)"]
P["프로세스\n(Process)"]
OS["OS 스케줄링\n(Time Slicing)"]
BG --> T
BG --> P
BG --> OS
T --- T_DESC["한 프로세스 안에서\n메모리 공유하며 분리"]
P --- P_DESC["완전히 별도 프로그램\nIPC로 통신"]
OS --- OS_DESC["CPU 시간을 나눠\n번갈아 실행"]1. 쓰레드 (Thread) — 한 프로세스 안에서 분리
쓰레드는 하나의 프로세스 내부에서 실행 흐름을 여러 갈래로 나누는 방식입니다. 같은 메모리 공간을 공유하기 때문에 데이터 교환이 빠르고 간단합니다.
flowchart TB
subgraph Process["내 노드 프로세스"]
direction TB
Main["메인 쓰레드\n제어 루프: 목표 계산 → IK → 발행"]
Callback["콜백 쓰레드\n/joint_states 수신 → 변수 갱신"]
Shared[("공유 메모리\nself._current_joints")]
end
Main <--> Shared
Callback <--> Shared핵심 특징:
- 같은 프로세스 안이므로 메모리를 공유
self._current_joints같은 변수를 쓰레드 간에 바로 읽고 쓸 수 있음- 대신 동기화 문제(Race Condition)를 주의해야 함
import threading
shared_data = {"joints": [0.0, 0.0, 0.0]}
lock = threading.Lock()
def callback_thread():
"""콜백 쓰레드: 센서 데이터를 갱신"""
while True:
new_data = receive_joint_states() # 데이터 수신
with lock: # 동기화: 동시 접근 방지
shared_data["joints"] = new_data
def main_thread():
"""메인 쓰레드: 제어 루프 실행"""
while True:
with lock:
current = shared_data["joints"] # 공유 데이터 읽기
command = compute_control(current)
publish(command)주의: 쓰레드 간 공유 변수에 동시 접근하면 데이터가 깨질 수 있습니다. 반드시
Lock,Mutex등으로 동기화를 처리하세요.
2. 프로세스 (Process) — 완전히 별도 프로그램
프로세스는 독립된 메모리 공간을 가진 별도의 프로그램입니다. 서로 직접 변수를 공유할 수 없고, IPC(Inter-Process Communication) 메커니즘을 통해 통신합니다.
flowchart LR
subgraph System["시스템"]
A["ros2_control\n프로세스"]
B["robot_state_publisher\n프로세스"]
C["move_group\n프로세스"]
D["내 노드\n프로세스"]
end
A -- "/joint_states\n(토픽)" --> D
B -- "/tf\n(토픽)" --> C
C -- "/compute_ik\n(서비스)" --> D핵심 특징:
- 각 프로세스는 독립된 메모리 공간을 가짐
- 하나가 죽어도 다른 프로세스에 직접적인 영향 없음
- ROS2에서는 토픽, 서비스, 액션 등으로 프로세스 간 통신
# ROS2에서 각각 별도 프로세스로 실행
ros2 run ros2_control_node ros2_control_node & # 프로세스 1
ros2 run robot_state_publisher robot_state_publisher & # 프로세스 2
ros2 run moveit2 move_group & # 프로세스 3
ros2 run my_pkg my_node & # 프로세스 43. OS 스케줄링 — sleep 중 다른 작업 처리
쓰레드를 명시적으로 만들지 않아도, 운영체제가 CPU 시간을 쪼개어 여러 작업을 번갈아 실행합니다. 특히 sleep() 호출 시 CPU를 자발적으로 반납하므로, OS가 다른 작업을 처리할 수 있습니다.
sequenceDiagram
participant Node as 내 노드
participant OS as 운영체제
participant Other as 다른 프로세스/쓰레드
Node->>OS: sleep(33ms) 호출
OS->>Other: CPU 할당 (다른 작업 처리)
Note over Other: 33ms 동안 실행
OS->>Node: 33ms 경과 → CPU 재할당
Node->>Node: 제어 루프 계속 실행핵심 특징:
- 쓰레드를 안 써도 OS가 알아서 시분할(Time Slicing) 처리
sleep()호출은 "나 잠깐 쉴게, 다른 거 해"라는 의미- 단일 코어에서도 여러 작업이 동시에 돌아가는 것처럼 보이는 이유
3가지 방식 비교
| 방식 | 메모리 | 통신 방법 | 비유 | 장점 | 단점 |
|---|---|---|---|---|---|
| 쓰레드 | 공유 | 변수 직접 접근 | 한 사무실에서 직원 여러 명이 일함 | 빠른 데이터 공유 | 동기화 필요 |
| 프로세스 | 독립 | IPC (토픽/서비스 등) | 다른 사무실에서 전화로 소통 | 격리성, 안정성 | 통신 오버헤드 |
| OS 스케줄링 | 해당 없음 | 해당 없음 | 직원 1명이 번갈아 처리 | 추가 구현 불필요 | 진정한 병렬 아님 |
ROS2에서의 실제 적용
ROS2에서는 Executor가 콜백 처리 방식을 결정합니다. Executor 종류에 따라 쓰레드 사용 여부가 달라집니다.
SingleThreadedExecutor — 쓰레드 1개
from rclpy.executors import SingleThreadedExecutor
executor = SingleThreadedExecutor()
executor.add_node(my_node)
executor.spin()
# → 콜백을 순서대로 하나씩 처리. 쓰레드 추가 생성 없음.sequenceDiagram
participant E as Executor (쓰레드 1개)
E->>E: 제어 루프 실행
E->>E: sleep(33ms)
E->>E: /joint_states 콜백 처리
E->>E: 제어 루프 재개
Note over E: 하나의 쓰레드에서 순차 실행- 콜백이 "백그라운드에서 도는 것처럼" 보이지만, 실제로는 메인 루프가 쉬는 동안 순차 처리
- 콜백이 오래 걸리면 제어 루프가 지연될 수 있음
MultiThreadedExecutor — 쓰레드 여러 개
from rclpy.executors import MultiThreadedExecutor
executor = MultiThreadedExecutor(num_threads=4)
executor.add_node(my_node)
executor.spin()
# → 콜백을 여러 쓰레드에서 동시 처리sequenceDiagram
participant T1 as 쓰레드 1
participant T2 as 쓰레드 2
participant T3 as 쓰레드 3
par 병렬 실행
T1->>T1: 제어 루프 실행
and
T2->>T2: /joint_states 콜백
and
T3->>T3: /tf 콜백
end
Note over T1,T3: 진짜 동시 실행 (멀티코어 활용)- 콜백과 제어 루프가 진짜 동시에 실행
- 대신 공유 변수 접근 시 동기화(Lock) 필수
Executor 선택 가이드
flowchart TD
Q1{"콜백 처리 시간이\n제어 주기보다 긴가?"}
Q2{"공유 데이터에\n동시 접근이 있는가?"}
Single["SingleThreadedExecutor\n간단하고 안전"]
Multi["MultiThreadedExecutor\n+ Lock 동기화"]
SingleOK["SingleThreadedExecutor\n충분히 빠름"]
Q1 -- "아니오" --> SingleOK
Q1 -- "예" --> Q2
Q2 -- "예" --> Multi
Q2 -- "아니오" --> Multi| 상황 | 추천 Executor |
|---|---|
| 콜백이 가볍고 빠름 | SingleThreadedExecutor |
| 콜백이 무겁거나 블로킹 | MultiThreadedExecutor |
| 실시간 제어 + 센서 수신 동시 | MultiThreadedExecutor + Lock |
실전 팁과 주의사항
- GIL (Global Interpreter Lock): Python에서는 GIL 때문에 CPU 바운드 작업은 멀티쓰레드로 성능 향상이 제한됩니다. I/O 바운드(네트워크, 센서 수신)에는 효과적입니다.
- 콜백 안에서 오래 걸리는 작업 금지: SingleThreadedExecutor에서는 하나의 콜백이 오래 걸리면 다른 콜백이 모두 밀립니다.
- ReentrantCallbackGroup: ROS2에서 같은 그룹의 콜백을 동시에 실행 가능하게 하려면
ReentrantCallbackGroup을 사용하세요. - 디버깅: 멀티쓰레드 버그(Race Condition, Deadlock)는 재현이 어렵습니다. 로깅을 충분히 남기고, 가능하면 단일 쓰레드부터 테스트하세요.
핵심 정리
"백그라운드"라는 말은 쓰레드만을 의미하지 않습니다.
| 현상 | 실제 메커니즘 |
|---|---|
/joint_states가 계속 갱신됨 | ros2_control이 별도 프로세스로 실행 |
| 내 노드 안에서 콜백이 처리됨 | Executor 방식에 따라 쓰레드일 수도, 순차 처리일 수도 있음 |
sleep() 중에도 시스템이 멈추지 않음 | OS 스케줄링이 CPU를 다른 작업에 할당 |
백그라운드 실행의 본질을 이해하면, 시스템 설계 시 어떤 방식을 선택할지 명확한 판단을 내릴 수 있습니다.